ESLint 설정하기
- 2025-08-17
- 저자: AK
그동안 ESLint 설정하느라 보낸 시간을 다 합치면 적어도 10시간, 최대 100시간은 될텐데 아직도 ESLint 설정 파일이 어떻게 작동하는지 정확히 모른다. 어떤 일에 100시간을 썼는데 쌓인 게 없으면 뭔가 잘못하고 있다는 뜻이다. 2시간 정도 진득하게 공부를 해봤다.
(2025년 8월 17일 v9.33.0 “Flat Config” 기준)
공부 환경 갖추기
뭘 고쳤을 때 어떻게 바뀌는지에 대한 빠른 피드백을 받는 게 중요하다.
설정 파일을 디버깅하는 방법에 대한 공식 문서를 읽어보니 npx eslint --inspect-config
라는 명령이 있다. 실행하면 현재 적용된 설정을 일목요연하게 보여주는 웹 페이지가 열린다. 설정 파일을 수정하면 페이지의 내용이 자동으로 갱신된다.
빈 설정에서 시작하기
기본 설정이 뭔지 알아보자.
import { defineConfig } from "eslint/config"
export default defineConfig([])
이렇게 했을 때의 기본 설정은 다음과 같다.
[
{
files: ["**/*.js", "**/*.mjs"],
ignores: [".git/", "**/node_modules/"],
languageOptions: {
sourceType: 'module',
ecmaVersion: 'latest'
},
linterOptions: {
reportUnusedDisableDirectives: 1
}
},
{
files: ["**/*.cjs"],
languageOptions: {
sourceType: 'commonjs',
ecmaVersion: 'latest'
}
}
]
모듈 JS(*.js
및 *.mjs
)와 커먼 JS(*.cjs
)의 소스 타입을 별도로 지정한 게 눈에 띈다. 아직은 아무런 규칙도 없다.
tseslint.config
typescript-eslint
패키지를 쓰려면 defineConfig()
대신에 tseslint.config()
를 쓰라고 안내하고 있다. 시키는대로 교체를 해도 설정에는 아무 변화가 없다. 찾아보니 기능엔 차이가 없고 defineConfig()
로 인해 발생하는 타입 문제를 잡아주기 위해 필요하다고 한다.
import tseslint from "typescript-eslint"
// eslint.defineConfig() has a types incompatibility issue
export default tseslint.config([])
여전히 아무런 규칙도 없는 상황.
Global ignores
소스코드 전체에 걸쳐 추가로 무시하고 싶은 파일들이 있다면 아무런 다른 키는 없고 오로지 ignores
만 있는 설정을 추가해야 한다고 하는데, name
은 추가해도 괜찮았다. (s4
는 현재 작업 중인 프로젝트 이름이다. 이런 식으로 ”/” 기호를 써주면 ESLint Config Inspector가 예쁘게 렌더링을 해준다.)
import tseslint from "typescript-eslint"
// eslint.defineConfig() has a types incompatibility issue
export default tseslint.config([
{
name: "s4/global ignores",
ignores: [".cursor/", ".github/", "dist/", "coverage/"],
// do not add anything else here to make `ignores` apply globally
},
])
name
이외의 다른 키(예: rules
)를 추가하면 ignores
는 더이상 글로벌로 작동하지 않는다. 실수하기 딱 좋다. 그래서 의도를 더 명확히 드러내고 실수 방지를 도와주기 위한 함수를 제공한다.
import { globalIgnores } from "eslint/config";
export default tseslint.config([
globalIgnores([".cursor/", ".github/", "dist/", "coverage/"]),
])
ESLint recommended
이제 규칙을 추가해보자.
@eslint/js
에는 all
과 recommended
설정이 담겨 있다. recommended
에 뭐가 있는지 console.log()
로 찍어보면 아래와 같다.
{
rules: {
'constructor-super': 'error',
'for-direction': 'error',
// ...중략...
'use-isnan': 'error',
'valid-typeof': 'error'
}
}
rules
만 정의하고 있는 자바스크립트 객체다. 이걸 아래와 같이 ESLint 설정에 추가하면 무슨 일이 벌어지나?
import js from "@eslint/js"
export default tseslint.config([
// ...중략...
js.configs.recommended,
])
인스펙터에서는 61개의 규칙이 “모든 파일”에 적용된다고 나온다. 왜 그럴까? 문서를 읽어보니 files
를 생략하면 다른 설정에서 지정한 모든 files
에 적용된다고 한다. ESLint 기본 설정이 *.js
, *.mjs
, *.cjs
를 명시하고 있으니 이 새 패턴의 파일들에 적용된다고 보면 되겠다.
한편, 문서에서 권장하는 방식은 아래와 같다.
[
// ...중략...
{ files: ["**/*.js"], plugins: { js }, extends: ["js/recommended"] },
]
다만 extends
에 문자열을 쓰는 방식은 eslint-typescript-plugin
의 config()
에서는 사용할 수 없으므로 아래와 같이 쓰면 된다.
[
// ...중략...
{ files: ["**/*.js"], extends: [js.configs.recommended] },
]
사실 js.config.recommended
는 {rules: […]}
형태의 객체이므로 아래와 같이 Spread 연산자 …
를 써도 된다.
[
// ...중략...
{ ...js.configs.recommended, files: ["**/*.js"] },
]
다만 규칙을 추가하거나 덮어쓰고 싶을 때 이런 식으로 번거로워질 수 있다.
{
...js.configs.recommended,
rules: {
...js.configs.recommended.rules,
"max-params": ["error", { "max": 5 }],
}
}
extends
를 쓰면 아래처럼이 깔끔하게 된다.
{
files: ["**/*.js"],
extends: [js.configs.recommended],
rules: {
"max-params": ["error", { "max": 5 }],
}
}
eslint-typescript-plugin: strictTypeChecked
eslint-typescript-plugin
의 “Typed Linting” 기능을 써서 타입 시스템을 바짝 조여보자. recommended
대신 strictTypeCheck
설정을 쓰면 된다.
다만 이걸 쓰려면 아래처럼 languageOptions
를 추가로 지정해줘야 한다.
{
extends: [ts.configs.strictTypeChecked],
languageOptions: { parserOptions: { projectService: true } },
rules: {
'@typescript-eslint/restrict-template-expressions': 'off',
"@typescript-eslint/switch-exhaustiveness-check": "error",
}
}
최종본
최종본. AI 에이전트가 만드는 코드의 품질을 강제할 목적으로 좀 과하게 조였다.
다만, 린터만으로는 한계가 있고 다른 수단들이 더 필요하다. 예(jscpd
: 중복 코드 감지; dependency-cruise
: 모듈 간 의존 구조 강제; knip
: 안쓰는 코드 감지. 자세한 내용은 에이전트 기반 코딩 실험 3 참고)
import js from "@eslint/js"
import { globalIgnores } from "eslint/config"
import importPlugin from "eslint-plugin-import"
import jsdoc from "eslint-plugin-jsdoc"
import sonarjs from "eslint-plugin-sonarjs"
import ts from "typescript-eslint"
// eslint.defineConfig() has a types incompatibility issue
export default ts.config([
// global ignores
globalIgnores([".cursor/", ".github/", "dist/", "coverage/", ".dependency-cruiser.cjs", "eslint.config.js"]),
// check for typescript files
{ name: "s4/ts", files: ["src/**/*.ts"] },
// js/recommended with custom rules
{
name: "s4/js-recomm-mod",
extends: [js.configs.recommended],
rules: {
"no-undef": "off",
"max-params": ["error", { max: 5 }],
"max-statements": ["error", { max: 15 }],
},
},
// jsdoc
{
name: "s4/jsdoc-recomm-mod",
extends: [jsdoc.configs["flat/recommended-error"]],
rules: {
"jsdoc/require-jsdoc": ["error", { publicOnly: true }],
"jsdoc/require-param-type": "off",
"jsdoc/require-returns-type": "off",
"jsdoc/require-returns-check": "error",
},
},
// import
{
name: "s4/import-recomm-mod",
extends: [importPlugin.flatConfigs.recommended],
ignores: ["eslint.config.js"],
rules: {
"import/max-dependencies": ["error", { max: 8, ignoreTypeImports: false }],
},
},
// typescript-eslint
{
name: "s4/ts-strict-type-checked-mod",
extends: [ts.configs.strictTypeChecked],
languageOptions: { parserOptions: { projectService: true } },
rules: {
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-magic-numbers": [
"error",
{
ignore: [-2, -1, 0, 1, 2, 10, 16, 24, 32, 42, 60, 100, 255, 256, 512, 1024],
ignoreEnums: true,
ignoreNumericLiteralTypes: true,
ignoreReadonlyClassProperties: true,
ignoreTypeIndexes: true,
},
],
},
},
// sonarjs
{
name: "s4/sonarjs-recomm-mod",
extends: [sonarjs.configs.recommended],
rules: {
"sonarjs/todo-tag": "off",
"sonarjs/pseudo-random": "off",
"sonarjs/no-os-command-from-path": "off",
"sonarjs/prefer-regexp-exec": "off",
"sonarjs/cognitive-complexity": ["error", 6],
"sonarjs/max-lines": ["error", { maximum: 200 }],
"sonarjs/elseif-without-else": "error",
"sonarjs/no-collapsible-if": "error",
"sonarjs/no-inconsistent-returns": "error",
"sonarjs/slow-regex": "off",
"no-useless-escape": "off",
"no-magic-numbers": "off",
},
},
// tests (overrides previous rules)
{
name: "s4/test",
files: ["src/**/*.test.ts"],
rules: {
"max-statements": ["error", { max: 20 }],
"sonarjs/cognitive-complexity": ["error", 3],
"sonarjs/max-lines": ["error", { maximum: 300 }],
},
},
])
몇 가지 재미난(?) 설정들:
- 테스트 케이스는 좀 길어져도 괜찮지만 인지복잡도는 프로덕션 코드에 비해 더 낮아야 함
- 전체 파일에 대해서는
max-lines
를 제야하고, 개별 함수에 대해서는max-statements
를 제약